#region Copyright & License Information
/*
 * Copyright 2007-2017 The OpenRA Developers (see AUTHORS)
 * This file is part of OpenRA, which is free software. It is made
 * available to you under the terms of the GNU General Public License
 * as published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version. For more
 * information, see COPYING.
 */
#endregion

using System.Linq;
using OpenRA.Mods.Common;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;

namespace OpenRA.Mods.MW.MWAI
{
	abstract class NavyStateBase : StateBase
	{
        protected virtual bool ShouldFlee(Squad owner)
        {
            return base.ShouldFlee(owner, enemies => !AttackOrFleeFuzzy.Default.CanAttack(owner.Units, enemies));
        }

        protected Actor FindClosestEnemy(Squad owner)
        {
            var first = owner.Units.First();

            // Navy squad AI can exploit enemy naval production to find path, if any.
            // (Way better than finding a nearest target which is likely to be on Ground)
            // You might be tempted to move these lookups into Activate() but that causes null reference exception.
            var domainIndex = first.World.WorldActor.Trait<DomainIndex>();
            var locomotorInfo = first.Info.TraitInfo<MobileInfo>().LocomotorInfo;

            var navalProductions = owner.World.ActorsHavingTrait<Building>().Where(a
                => owner.Bot.Info.BuildingCommonNames.NavalProduction.Contains(a.Info.Name)
                && domainIndex.IsPassable(first.Location, a.Location, locomotorInfo)
                && a.AppearsHostileTo(first));

            if (navalProductions.Any())
            {
                var nearest = navalProductions.ClosestTo(first);

                // Return nearest when it is FAR enough.
                // If the naval production is within MaxBaseRadius, it implies that
                // this squad is close to enemy territory and they should expect a naval combat;
                // closest enemy makes more sense in that case.
                if ((nearest.Location - first.Location).LengthSquared > owner.Bot.Info.MaxBaseRadius * owner.Bot.Info.MaxBaseRadius)
                    return nearest;
            }

            return owner.Bot.FindClosestEnemy(first.CenterPosition);
        }
    }

    class NavyUnitsIdleState : NavyStateBase, IState
    {
        public void Activate(Squad owner) { }

        public void Tick(Squad owner)
        {
            if (!owner.IsValid)
                return;

            if (!owner.IsTargetValid)
            {
                var closestEnemy = FindClosestEnemy(owner);
                if (closestEnemy == null)
                    return;

                owner.TargetActor = closestEnemy;
            }

            var enemyUnits = owner.World.FindActorsInCircle(owner.TargetActor.CenterPosition, WDist.FromCells(owner.Bot.Info.IdleScanRadius))
                .Where(unit => owner.Bot.Player.Stances[unit.Owner] == Stance.Enemy).ToList();

            if (enemyUnits.Count == 0)
                return;

            if (AttackOrFleeFuzzy.Default.CanAttack(owner.Units, enemyUnits))
            {
                foreach (var u in owner.Units)
                    owner.Bot.QueueOrder(new Order("AttackMove", u, Target.FromCell(owner.World, owner.TargetActor.Location), false));

                // We have gathered sufficient units. Attack the nearest enemy unit.
                owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsAttackMoveState(), true);
            }
            else
                owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsFleeState(), true);
        }

        public void Deactivate(Squad owner) { }
    }

    class NavyUnitsAttackMoveState : NavyStateBase, IState
    {
        public void Activate(Squad owner) { }

        public void Tick(Squad owner)
        {
            if (!owner.IsValid)
                return;

            if (!owner.IsTargetValid)
            {
                var closestEnemy = FindClosestEnemy(owner);
                if (closestEnemy != null)
                    owner.TargetActor = closestEnemy;
                else
                {
                    owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsFleeState(), true);
                    return;
                }
            }

            var leader = owner.Units.ClosestTo(owner.TargetActor.CenterPosition);
            if (leader == null)
                return;

            var ownUnits = owner.World.FindActorsInCircle(leader.CenterPosition, WDist.FromCells(owner.Units.Count) / 3)
                .Where(a => a.Owner == owner.Units.First().Owner && owner.Units.Contains(a)).ToHashSet();

            if (ownUnits.Count < owner.Units.Count)
            {
                // Since units have different movement speeds, they get separated while approaching the target.
                // Let them regroup into tighter formation.
                owner.Bot.QueueOrder(new Order("Stop", leader, false));
                foreach (var unit in owner.Units.Where(a => !ownUnits.Contains(a)))
                    owner.Bot.QueueOrder(new Order("AttackMove", unit, Target.FromCell(owner.World, leader.Location), false));
            }
            else
            {
                var enemies = owner.World.FindActorsInCircle(leader.CenterPosition, WDist.FromCells(owner.Bot.Info.AttackScanRadius))
                    .Where(a => !a.IsDead && leader.Owner.Stances[a.Owner] == Stance.Enemy && !a.GetEnabledTargetTypes().IsEmpty);
                var target = enemies.ClosestTo(leader.CenterPosition);
                if (target != null)
                {
                    owner.TargetActor = target;
                    owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsAttackState(), true);
                }
                else
                    foreach (var a in owner.Units)
                        owner.Bot.QueueOrder(new Order("AttackMove", a, Target.FromCell(owner.World, owner.TargetActor.Location), false));
            }

            if (ShouldFlee(owner))
                owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsFleeState(), true);
        }

        public void Deactivate(Squad owner) { }
    }

    class NavyUnitsAttackState : NavyStateBase, IState
    {
        public void Activate(Squad owner) { }

        public void Tick(Squad owner)
        {
            if (!owner.IsValid)
                return;

            if (!owner.IsTargetValid)
            {
                var closestEnemy = FindClosestEnemy(owner);
                if (closestEnemy != null)
                    owner.TargetActor = closestEnemy;
                else
                {
                    owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsFleeState(), true);
                    return;
                }
            }

            foreach (var a in owner.Units)
                if (!BusyAttack(a))
                    owner.Bot.QueueOrder(new Order("Attack", a, Target.FromActor(owner.TargetActor), false));

            if (ShouldFlee(owner))
                owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsFleeState(), true);
        }

        public void Deactivate(Squad owner) { }
    }

    class NavyUnitsFleeState : NavyStateBase, IState
    {
        public void Activate(Squad owner) { }

        public void Tick(Squad owner)
        {
            if (!owner.IsValid)
                return;

            GoToRandomOwnBuilding(owner);
            owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsIdleState(), true);
        }

        public void Deactivate(Squad owner) { owner.Units.Clear(); }
    }
}
